Memory<T> and ReadOnlyMemory<T>
Heap-safe counterparts to Span for async and storage scenarios
Memory and ReadOnlyMemory
Master modern .NET memory allocation with free flashcards and spaced repetition practice. This lesson covers Memory
π» Memory
Welcome to Modern Memory Management
Traditional .NET development relied heavily on arrays and strings, which always allocate on the heap and create garbage collection pressure. When you needed to pass around subsets of data, you'd either copy the data (expensive) or pass indices around (error-prone). Memory
πΊ Think of Memory
- A managed array on the heap
- Native memory allocated outside the GC
- Memory-mapped files
- Stack-allocated memory (when converted from Span
)
Core Concepts
What Are Memory and ReadOnlyMemory?
Memory
| Feature | Memory | ReadOnlyMemory |
|---|---|---|
| Storage location | Stack (value type) | Stack (value type) |
| Can be field | β Yes | β Yes |
| Async support | β Yes | β Yes |
| Modification | β Mutable | β Read-only |
| GC tracking | β Yes (if managed) | β Yes (if managed) |
Key Properties and Methods
Both types expose similar APIs:
Properties:
Length- Number of elements in the memory regionIsEmpty- Returns true if Length is 0Span- Gets a Spanor ReadOnlySpan over the memory
Methods:
Slice(start, length)- Creates a new Memoryover a portion (zero-copy) CopyTo(Memory<T>)- Copies contents to another memory regionPin()- Returns a MemoryHandle for interop scenariosToArray()- Creates a new array copy of the data
The Relationship with Span
π‘ Critical distinction: Memory.Span property.
βββββββββββββββββββββββββββββββββββββββββββββββ
β MEMORY TYPE HIERARCHY β
βββββββββββββββββββββββββββββββββββββββββββββββ
Memory ReadOnlyMemory
(mutable) (immutable)
β β
β .Span property β .Span property
β β
Span ReadOnlySpan
(ref struct) (ref struct)
β β
ββββββββββ¬βββββββββββββββββ
β
β
Actual data manipulation
(indexing, iteration, etc.)
π§ Memory device: Think "Memory can be Moved around" (stored anywhere), while "Span is Stuck on the stack" (ref struct limitations).
Creating Memory Instances
There are several ways to create Memory
From arrays:
int[] numbers = { 1, 2, 3, 4, 5 };
Memory<int> memory = numbers; // Implicit conversion
Memory<int> slice = numbers.AsMemory(1, 3); // Elements [1,2,3]
From strings:
string text = "Hello, World!";
ReadOnlyMemory<char> memory = text.AsMemory();
ReadOnlyMemory<char> hello = text.AsMemory(0, 5); // "Hello"
Using MemoryPool
using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(1024);
Memory<byte> memory = owner.Memory;
// Use memory...
// Automatically returned when owner is disposed
From ArrayPool
byte[] rented = ArrayPool<byte>.Shared.Rent(4096);
Memory<byte> memory = rented.AsMemory(0, 1024);
// Use memory...
ArrayPool<byte>.Shared.Return(rented);
Zero-Copy Slicing Operations
One of the most powerful features is slicingβcreating views over subsets of memory without copying data:
int[] data = { 10, 20, 30, 40, 50, 60, 70, 80 };
Memory<int> memory = data;
// All these create views, NO copying occurs
Memory<int> first4 = memory.Slice(0, 4); // [10,20,30,40]
Memory<int> middle = memory.Slice(2, 4); // [30,40,50,60]
Memory<int> last3 = memory.Slice(5); // [60,70,80]
// Modifications through any slice affect the original
first4.Span[0] = 99;
Console.WriteLine(data[0]); // Outputs: 99
Original array: [10β20β30β40β50β60β70β80]
βββββββββ¬ββββββββ
first4 slice
(no copy!)
Memory layout visualization:
βββββββββββββββββββββββββββββββββββββββββββ
β 10 β 20 β 30 β 40 β 50 β 60 β 70 β 80 β β Actual data
βββββββββββββββββββββββββββββββββββββββββββ
β β
β β
memory (entire array) memory.Slice(5)
β points here
ββ first4.Span[0] = 99 modifies this
Working with ReadOnlyMemory
ReadOnlyMemory
public class DataProcessor
{
private readonly ReadOnlyMemory<byte> _data;
public DataProcessor(byte[] data)
{
// Defensive copy NOT needed - ReadOnlyMemory prevents mutation
_data = data;
}
public int CalculateChecksum()
{
ReadOnlySpan<byte> span = _data.Span;
int checksum = 0;
foreach (byte b in span)
{
checksum ^= b;
}
return checksum;
}
}
π Immutability guarantee: Consumers cannot modify data through ReadOnlyMemory
Detailed Examples
Example 1: High-Performance String Parsing
Traditional string parsing creates many temporary strings (garbage). Memory
public static class CsvParser
{
public static List<ReadOnlyMemory<char>> ParseLine(ReadOnlyMemory<char> line)
{
var fields = new List<ReadOnlyMemory<char>>();
int start = 0;
ReadOnlySpan<char> span = line.Span;
for (int i = 0; i < span.Length; i++)
{
if (span[i] == ',')
{
// Slice creates a view - no string allocation!
fields.Add(line.Slice(start, i - start));
start = i + 1;
}
}
// Don't forget the last field
if (start < span.Length)
{
fields.Add(line.Slice(start));
}
return fields;
}
}
// Usage:
string csvLine = "John,Doe,42,Engineer";
ReadOnlyMemory<char> memory = csvLine.AsMemory();
List<ReadOnlyMemory<char>> fields = CsvParser.ParseLine(memory);
// Convert to string only when needed
string firstName = fields[0].ToString(); // "John"
string age = fields[2].ToString(); // "42"
Performance benefit: Traditional Split(',') creates 4 string objects on the heap. This approach creates zero heap allocations until you explicitly call ToString().
Example 2: Async I/O with Memory
Memory
public class AsyncFileProcessor
{
private readonly Memory<byte> _buffer;
public AsyncFileProcessor(int bufferSize = 4096)
{
_buffer = new byte[bufferSize];
}
public async Task<int> ReadAndProcessAsync(Stream stream)
{
// Memory<T> works in async methods - Span<T> does NOT!
int bytesRead = await stream.ReadAsync(_buffer);
if (bytesRead == 0)
return 0;
// Now convert to Span for actual processing
Span<byte> dataToProcess = _buffer.Span.Slice(0, bytesRead);
// Process the data
int processedCount = ProcessBytes(dataToProcess);
return processedCount;
}
private int ProcessBytes(Span<byte> data)
{
int count = 0;
for (int i = 0; i < data.Length; i++)
{
if (data[i] > 127) // Example: count non-ASCII bytes
count++;
}
return count;
}
}
β‘ Key insight: Memory
| Scenario | Use This | Why |
|---|---|---|
| Async method | Memory | Can survive await points |
| Synchronous processing | Span | Slightly faster, stack-only |
| Class field | Memory | Ref structs can't be fields |
| Local variable | Either | Both work; Span |
Example 3: Memory Pooling for Reduced GC Pressure
Combining Memory
public class PacketProcessor
{
public async Task ProcessPacketsAsync(NetworkStream stream)
{
// Rent from shared pool instead of allocating
using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(2048);
Memory<byte> buffer = owner.Memory;
while (true)
{
// Read packet header
int headerBytes = await stream.ReadAsync(buffer.Slice(0, 4));
if (headerBytes == 0) break;
// Parse packet length from header
int packetLength = BitConverter.ToInt32(buffer.Span.Slice(0, 4));
if (packetLength > buffer.Length - 4)
{
throw new InvalidOperationException("Packet too large");
}
// Read packet body
int bodyBytes = await stream.ReadAsync(
buffer.Slice(4, packetLength));
// Process the complete packet
ProcessPacket(buffer.Span.Slice(0, 4 + packetLength));
}
// Memory automatically returned to pool on dispose
}
private void ProcessPacket(ReadOnlySpan<byte> packet)
{
// Packet processing logic
Console.WriteLine($"Processed packet of {packet.Length} bytes");
}
}
π Performance impact:
- Traditional approach: Allocates array per packet β frequent GC
- Pooled approach: Reuses memory β minimal GC, 2-5x faster throughput
Example 4: Slicing Protocol Messages
Parsing binary protocols becomes elegant with Memory
public readonly struct HttpHeader
{
public ReadOnlyMemory<char> Name { get; }
public ReadOnlyMemory<char> Value { get; }
public HttpHeader(ReadOnlyMemory<char> name, ReadOnlyMemory<char> value)
{
Name = name;
Value = value;
}
}
public static class HttpParser
{
public static List<HttpHeader> ParseHeaders(ReadOnlyMemory<char> headerBlock)
{
var headers = new List<HttpHeader>();
ReadOnlyMemory<char> remaining = headerBlock;
while (!remaining.IsEmpty)
{
// Find line break
ReadOnlySpan<char> span = remaining.Span;
int lineEnd = span.IndexOf('\n');
if (lineEnd == -1) break;
ReadOnlyMemory<char> line = remaining.Slice(0, lineEnd);
remaining = remaining.Slice(lineEnd + 1);
// Find colon separator
ReadOnlySpan<char> lineSpan = line.Span;
int colonPos = lineSpan.IndexOf(':');
if (colonPos == -1) continue;
// Split into name and value (zero-copy!)
ReadOnlyMemory<char> name = line.Slice(0, colonPos);
ReadOnlyMemory<char> value = line.Slice(colonPos + 1).Trim();
headers.Add(new HttpHeader(name, value));
}
return headers;
}
}
// Usage:
string rawHeaders = "Content-Type: application/json\nContent-Length: 1234\n";
ReadOnlyMemory<char> headerMemory = rawHeaders.AsMemory();
List<HttpHeader> headers = HttpParser.ParseHeaders(headerMemory);
foreach (var header in headers)
{
// Convert to string only when displaying
Console.WriteLine($"{header.Name}: {header.Value}");
}
π― Design principle: Keep data as Memory
Common Mistakes
β οΈ Mistake 1: Holding Memory After Source Is Disposed
// β WRONG - Dangerous!
Memory<byte> GetBuffer()
{
using var owner = MemoryPool<byte>.Shared.Rent(1024);
return owner.Memory; // Memory becomes invalid after method returns!
}
// β
RIGHT - Return the owner or copy the data
IMemoryOwner<byte> GetBuffer()
{
return MemoryPool<byte>.Shared.Rent(1024);
// Caller is responsible for disposal
}
β οΈ Mistake 2: Using Span When You Need Memory
// β WRONG - Won't compile!
public class DataCache
{
private Span<byte> _cachedData; // Error: Can't use ref struct as field
}
// β
RIGHT - Use Memory<T> for fields
public class DataCache
{
private Memory<byte> _cachedData;
public void Cache(byte[] data)
{
_cachedData = data;
}
}
β οΈ Mistake 3: Unnecessary Copying
// β WRONG - Creates unnecessary copy
public void ProcessData(ReadOnlyMemory<char> input)
{
string text = input.ToString(); // Allocates string
char[] chars = text.ToCharArray(); // Another allocation!
// Process chars...
}
// β
RIGHT - Work with Span directly
public void ProcessData(ReadOnlyMemory<char> input)
{
ReadOnlySpan<char> span = input.Span; // Zero allocation
// Process span directly...
}
β οΈ Mistake 4: Forgetting ReadOnly Variants
// β WRONG - Mutable when it should be immutable
public Memory<char> GetProductName()
{
return _productName; // Callers can modify!
}
// β
RIGHT - Use ReadOnly for immutable data
public ReadOnlyMemory<char> GetProductName()
{
return _productName; // Safe from modification
}
β οΈ Mistake 5: Pinning Without Disposal
// β WRONG - Memory handle leaks
public void UseNativeApi(Memory<byte> data)
{
MemoryHandle handle = data.Pin();
// ... use handle.Pointer ...
// Forgot to dispose!
}
// β
RIGHT - Always dispose MemoryHandle
public void UseNativeApi(Memory<byte> data)
{
using MemoryHandle handle = data.Pin();
// ... use handle.Pointer ...
} // Automatically unpinned
Key Takeaways
β
Memory
β
Zero-copy operations: Slicing with .Slice() creates views over existing memory without copying dataβcritical for high-performance scenarios.
β
ReadOnly variants: Use ReadOnlyMemory
β
Pooling integration: Combine Memory
β
Async compatibility: Memory
β
Conversion pattern: Store as Memory
π Quick Reference Card
| Type | Storage | Async | Mutable |
| Memory<T> | Field/Property | β Yes | β Yes |
| ReadOnlyMemory<T> | Field/Property | β Yes | β No |
| Span<T> | Stack only | β No | β Yes |
| ReadOnlySpan<T> | Stack only | β No | β No |
Common conversions:
array.AsMemory()β Memory<T>memory.Spanβ Span<T>memory.Slice(start, length)β sliced Memory<T>memory.ToArray()β new array copy
When to use:
- Memory<T>: Async methods, class fields, long-lived references
- Span<T>: Synchronous processing, maximum performance
- ReadOnly*: Immutable data, public APIs, thread-safe sharing
π Further Study
Microsoft Docs - Memory<T> and Span<T> usage guidelines: https://learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines
Stephen Toub's article on Memory<T> and Span<T>: https://learn.microsoft.com/en-us/archive/msdn-magazine/2018/january/csharp-all-about-span-exploring-a-new-net-mainstay
Adam Sitnik's performance benchmarks: https://adamsitnik.com/Span/
π‘ Pro tip: Use BenchmarkDotNet to measure the performance impact of Memory
π§ Try this: Convert an existing string parsing method to use ReadOnlyMemory